استكشف التقنيات المتقدمة لجلب البيانات المتوازية في React باستخدام Suspense، مما يعزز أداء التطبيق وتجربة المستخدم. تعلم استراتيجيات لتنسيق عمليات غير متزامنة متعددة وإدارة حالات التحميل بفعالية.
تنسيق React Suspense: إتقان استراتيجيات جلب البيانات المتوازية
لقد أحدث React Suspense ثورة في طريقة تعاملنا مع العمليات غير المتزامنة، وخاصة جلب البيانات. يسمح للمكونات "بتعليق" العرض أثناء انتظار تحميل البيانات، مما يوفر طريقة تعريفية لإدارة حالات التحميل. ومع ذلك، فإن مجرد تغليف عمليات جلب البيانات الفردية باستخدام Suspense يمكن أن يؤدي إلى تأثير الشلال، حيث يكتمل جلب واحد قبل أن يبدأ التالي، مما يؤثر سلبًا على الأداء. يتعمق منشور المدونة هذا في استراتيجيات متقدمة لتنسيق عمليات جلب بيانات متعددة بشكل متوازٍ باستخدام Suspense، وتحسين استجابة تطبيقك وتعزيز تجربة المستخدم لجمهور عالمي.
فهم مشكلة الشلال في جلب البيانات
تخيل سيناريو حيث تحتاج إلى عرض ملف تعريف المستخدم مع اسمه وصورته المصغرة وأنشطته الأخيرة. إذا قمت بجلب كل جزء من البيانات بشكل تسلسلي، يرى المستخدم مؤشر تحميل للاسم، ثم واحد آخر للصورة المصغرة، وأخيرًا، واحد لملف النشاط. يخلق نمط التحميل التسلسلي هذا تأثير الشلال، مما يؤخر عرض الملف الشخصي الكامل ويحبط المستخدمين. بالنسبة للمستخدمين الدوليين الذين لديهم سرعات شبكة مختلفة، يمكن أن يكون هذا التأخير أكثر وضوحًا.
ضع في اعتبارك مقتطف التعليمات البرمجية المبسط هذا:
function UserProfile() {
const name = useName(); // fetches user name
const avatar = useAvatar(name); // fetches avatar based on name
const activity = useActivity(name); // fetches activity based on name
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="User Avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
في هذا المثال، تعتمد useAvatar و useActivity على نتيجة useName. هذا يخلق شلالًا واضحًا - لا يمكن لـ useAvatar و useActivity البدء في جلب البيانات حتى يكتمل useName. هذا غير فعال وهو عنق زجاجة شائع في الأداء.
استراتيجيات جلب البيانات المتوازية باستخدام Suspense
المفتاح لتحسين جلب البيانات باستخدام Suspense هو بدء جميع طلبات البيانات بشكل متزامن. إليك العديد من الاستراتيجيات التي يمكنك استخدامها:
1. التحميل المسبق للبيانات باستخدام React.preload والموارد
إحدى أقوى التقنيات هي التحميل المسبق للبيانات قبل أن يقوم المكون بالعرض. يتضمن ذلك إنشاء "مورد" (كائن يغلف وعد جلب البيانات) وجلب البيانات مسبقًا. يساعد React.preload في هذا. بحلول الوقت الذي يحتاج فيه المكون إلى البيانات، تكون متاحة بالفعل، مما يلغي حالة التحميل تقريبًا.
ضع في اعتبارك موردًا لجلب منتج:
const createProductResource = (productId) => {
let promise;
let product;
let error;
const suspender = new Promise((resolve, reject) => {
promise = fetch(`/api/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
product = data;
resolve();
})
.catch(e => {
error = e;
reject(e);
});
});
return {
read() {
if (error) {
throw error;
}
if (product) {
return product;
}
throw suspender;
},
};
};
// Usage:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
الآن، يمكنك تحميل هذا المورد مسبقًا قبل عرض مكون ProductDetails. على سبيل المثال، أثناء انتقالات المسار أو عند التحويم.
React.preload(productResource);
يضمن هذا أن البيانات متاحة على الأرجح عندما يحتاجها مكون ProductDetails، مما يقلل من حالة التحميل أو يلغيها.
2. استخدام Promise.all لجلب البيانات المتزامنة
نهج آخر بسيط وفعال هو استخدام Promise.all لبدء جميع عمليات جلب البيانات بشكل متزامن داخل حدود Suspense واحدة. يعمل هذا بشكل جيد عندما تكون تبعيات البيانات معروفة مسبقًا.
دعنا نعود إلى مثال ملف تعريف المستخدم. بدلاً من جلب البيانات بشكل تسلسلي، يمكننا جلب الاسم والصورة المصغرة والنشاط بشكل متزامن:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
function Name() {
const name = useSuspense(fetchName());
return <h2>{name}</h2>;
}
function Avatar({ name }) {
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity({ name }) {
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const name = useSuspense(fetchName());
return (
<div>
<Suspense fallback=<div>Loading Avatar...</div>>
<Avatar name={name} />
</Suspense>
<Suspense fallback=<div>Loading Activity...</div>>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
ومع ذلك، إذا كانت كل من Avatar و Activity تعتمدان أيضًا على fetchName، ولكن تم عرضهما داخل حدود Suspense منفصلة، يمكنك رفع وعد fetchName إلى المكون الأصل وتوفيره عبر React Context.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
const NamePromiseContext = createContext(null);
function Avatar() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const namePromise = fetchName();
return (
<NamePromiseContext.Provider value={namePromise}>
<Suspense fallback=<div>Loading Avatar...</div>>
<Avatar />
</Suspense>
<Suspense fallback=<div>Loading Activity...</div>>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. استخدام خطاف مخصص لإدارة عمليات الجلب المتوازية
للسيناريوهات الأكثر تعقيدًا مع تبعيات بيانات شرطية محتملة، يمكنك إنشاء خطاف مخصص لإدارة جلب البيانات المتوازية وإرجاع مورد يمكن لـ Suspense استخدامه.
import { useState, useEffect, useRef } from 'react';
function useParallelData(fetchFunctions) {
const [resource, setResource] = useState(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
const promises = fetchFunctions.map(fn => fn());
const suspender = Promise.all(promises).then(
(results) => {
if (mounted.current) {
setResource({ status: 'success', value: results });
}
},
(error) => {
if (mounted.current) {
setResource({ status: 'error', value: error });
}
}
);
setResource({
status: 'pending',
value: suspender,
});
return () => {
mounted.current = false;
};
}, [fetchFunctions]);
const read = () => {
if (!resource) {
throw new Error('Resource not yet initialized');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// Example usage:
async function fetchUserData(userId) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'User ' + userId };
}
async function fetchUserPosts(userId) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 500));
return [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }];
}
function UserProfile({ userId }) {
const { read } = useParallelData([
() => fetchUserData(userId),
() => fetchUserPosts(userId),
]);
const [userData, userPosts] = read();
return (
<div>
<h2>{userData.name}</h2>
<ul>
{userPosts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback=<div>Loading user data...</div>>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
يغلف هذا النهج تعقيد إدارة الوعود وحالات التحميل داخل الخطاف، مما يجعل كود المكون أنظف وأكثر تركيزًا على عرض البيانات.
4. الترطيب الانتقائي مع عرض الخادم المتدفق
بالنسبة للتطبيقات التي يتم عرضها على الخادم، يقدم React 18 الترطيب الانتقائي مع عرض الخادم المتدفق. هذا يسمح لك بإرسال HTML إلى العميل في أجزاء عند توفرها على الخادم. يمكنك تغليف المكونات التي يتم تحميلها ببطء بحدود <Suspense>، مما يسمح لبقية الصفحة بأن تصبح تفاعلية بينما لا تزال المكونات البطيئة تحمل على الخادم. هذا يحسن بشكل كبير الأداء المتصور، خاصة للمستخدمين الذين لديهم اتصالات شبكة بطيئة أو أجهزة.
ضع في اعتبارك سيناريو حيث يحتاج موقع إخباري إلى عرض مقالات من مناطق مختلفة من العالم (مثل آسيا وأوروبا والأمريكتين). قد تكون بعض مصادر البيانات أبطأ من غيرها. يسمح الترطيب الانتقائي بعرض المقالات من المناطق الأسرع أولاً، بينما لا تزال تلك من المناطق الأبطأ قيد التحميل، مما يمنع حظر الصفحة بأكملها.
معالجة الأخطاء وحالات التحميل
بينما يبسط Suspense إدارة حالات التحميل، تظل معالجة الأخطاء أمرًا بالغ الأهمية. تسمح لك حدود الأخطاء (باستخدام طريقة دورة حياة componentDidCatch أو خطاف useErrorBoundary من مكتبات مثل react-error-boundary) بمعالجة الأخطاء التي تحدث أثناء جلب البيانات أو العرض بشكل رشيق. يجب وضع حدود الأخطاء هذه بشكل استراتيجي لالتقاط الأخطاء داخل حدود Suspense محددة، مما يمنع تعطل التطبيق بأكمله.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... fetches data that might error
}
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong!</div>}>
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
تذكر توفير واجهة مستخدم احتياطية مفيدة وسهلة الاستخدام لكل من حالات التحميل والأخطاء. هذا مهم بشكل خاص للمستخدمين الدوليين الذين قد يواجهون سرعات شبكة أبطأ أو انقطاعات خدمة إقليمية.
أفضل الممارسات لتحسين جلب البيانات باستخدام Suspense
- تحديد البيانات الهامة وتحديد أولوياتها: حدد البيانات الضرورية للعرض الأولي لتطبيقك وحدد أولويات جلب تلك البيانات أولاً.
- تحميل البيانات مسبقًا عندما يكون ذلك ممكنًا: استخدم
React.preloadوالموارد لتحميل البيانات مسبقًا قبل أن تحتاجها المكونات، مما يقلل من حالات التحميل. - جلب البيانات بشكل متزامن: استخدم
Promise.allأو الخطافات المخصصة لبدء عمليات جلب بيانات متعددة بالتوازي. - تحسين نقاط نهاية API: تأكد من تحسين نقاط نهاية API الخاصة بك للأداء، وتقليل زمن الوصول وحجم حمولة البيانات. فكر في استخدام تقنيات مثل GraphQL لجلب البيانات التي تحتاجها فقط.
- تنفيذ التخزين المؤقت: قم بتخزين البيانات التي يتم الوصول إليها بشكل متكرر مؤقتًا لتقليل عدد طلبات API. فكر في استخدام مكتبات مثل
swrأوreact-queryلإمكانيات تخزين مؤقت قوية. - استخدام تقسيم الكود: قسم تطبيقك إلى أجزاء أصغر لتقليل وقت التحميل الأولي. اجمع بين تقسيم الكود و Suspense لتحميل وعرض أجزاء مختلفة من تطبيقك تدريجيًا.
- مراقبة الأداء: راقب أداء تطبيقك بانتظام باستخدام أدوات مثل Lighthouse أو WebPageTest لتحديد ومعالجة اختناقات الأداء.
- معالجة الأخطاء برشاقة: قم بتنفيذ حدود الأخطاء لالتقاط الأخطاء أثناء جلب البيانات والعرض، وتوفير رسائل خطأ إعلامية للمستخدمين.
- النظر في عرض جانب الخادم (SSR): لأسباب تتعلق بتحسين محركات البحث والأداء، فكر في استخدام SSR مع التدفق والترطيب الانتقائي لتقديم تجربة أولية أسرع.
الخلاصة
يوفر React Suspense، عند دمجه مع استراتيجيات جلب البيانات المتوازية، مجموعة أدوات قوية لبناء تطبيقات ويب سريعة الاستجابة وعالية الأداء. من خلال فهم مشكلة الشلال وتطبيق تقنيات مثل التحميل المسبق، والجلب المتزامن باستخدام Promise.all، والخطافات المخصصة، يمكنك تحسين تجربة المستخدم بشكل كبير. تذكر معالجة الأخطاء برشاقة ومراقبة الأداء لضمان بقاء تطبيقك محسنًا للمستخدمين في جميع أنحاء العالم. مع استمرار تطور React، سيؤدي استكشاف الميزات الجديدة مثل الترطيب الانتقائي مع عرض الخادم المتدفق إلى تعزيز قدرتك على تقديم تجارب مستخدم استثنائية، بغض النظر عن الموقع أو ظروف الشبكة. من خلال تبني هذه التقنيات، يمكنك إنشاء تطبيقات ليست وظيفية فحسب، بل أيضًا مبهجة للاستخدام لجمهورك العالمي.
يهدف منشور المدونة هذا إلى تقديم نظرة عامة شاملة لاستراتيجيات جلب البيانات المتوازية مع React Suspense. نأمل أن تجدها مفيدة وغنية بالمعلومات. نشجعك على تجربة هذه التقنيات في مشاريعك الخاصة ومشاركة اكتشافاتك مع المجتمع.